package com.henriquevilela.drumtrainer.core;

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MetaEventListener;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Synthesizer;
import javax.sound.midi.Track;
import javax.swing.Timer;

import com.henriquevilela.drumtrainer.core.input.MIDIInputDevice;
import com.henriquevilela.drumtrainer.core.input.MIDIInputDeviceListener;
import com.sun.media.sound.MidiUtils;

// http://www.io.com/~jimm/midi_ref.html#GM_Patches
// http://www.io.com/~jimm/midi_ref.html#GM_Percussion

public class DrumTrainerEngine implements MIDIInputDeviceListener, MetaEventListener {

	private MIDIInputDevice input;

	private Sequence sequence;

	private Sequencer sequencer;

	private Synthesizer synthesizer;

	private List<PlaySymbol> playSymbolList;

	private Map<Integer, Symbol> symbolMap;

	private int startIndex = 0;

	private List<Symbol> symbolList;

	private long tolerance = 100000;

	private List<DrumTrainerListener> listenerList;

	private Timer timer;

	private int rigthHit;

	private int wrongHit;

	private List<long[]> timing;

	public DrumTrainerEngine() throws MidiUnavailableException {
		input = new MIDIInputDevice();
		input.addMIDIInputDeviceListener(this);

		synthesizer = MidiSystem.getSynthesizer();
		synthesizer.open();
		
		symbolMap = new HashMap<Integer, Symbol>();
		symbolList = new Vector<Symbol>();

		setDefatultDrumSet();

		listenerList = new Vector<DrumTrainerListener>();

		timer = new javax.swing.Timer(10, new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				tick();
			}
		});
		
		timing = new Vector<long[]>();
	}

	public void addDrumEngineListener(DrumTrainerListener listener) {
		listenerList.add(listener);
	}

	public void removeDrumEngineListener(DrumTrainerListener listener) {
		listenerList.remove(listener);
	}

	public List<MidiDevice.Info> getAvaliableDeviceList() {
		return input.getAvaliableDeviceList();
	}

	private void setDefatultDrumSet() {
		Symbol acousticSnare = new Symbol("Snare", 0, Color.RED);
		symbolList.add(acousticSnare);
		symbolMap.put(38, acousticSnare); // Acoustic Snare
		symbolMap.put(40, acousticSnare); // Acoustic Snare

		Symbol ride = new Symbol("Ride", 1, Color.YELLOW);
		symbolList.add(ride);
		symbolMap.put(42, ride); // Closed Hi Hat
		symbolMap.put(44, ride); // Pedal Hi-Hat
		symbolMap.put(46, ride); // Open Hi-Hat
		symbolMap.put(51, ride); // Ride Cymbal 1
		symbolMap.put(53, ride); // Ride Bell
		symbolMap.put(54, ride); // Tambourine  	
		symbolMap.put(56, ride); // Cowbell
		symbolMap.put(59, ride); // Ride Cymbal 2

		Symbol hiTom = new Symbol("Hi Tom", 2, Color.BLUE);
		symbolList.add(hiTom);
		symbolMap.put(41, hiTom); // Low Floor Tom
		symbolMap.put(48, hiTom); // Hi Mid Tom
		symbolMap.put(50, hiTom); // High Tom

		//Symbol lowTom = new Symbol("Low Tom", 3);
		//symbolList.add(lowTom);

		Symbol crashCymbal = new Symbol("Crash", 3, Color.ORANGE);
		symbolList.add(crashCymbal);
		symbolMap.put(49, crashCymbal); // Crash Cymbal 1
		symbolMap.put(52, crashCymbal); // Chinese Cymbal
		symbolMap.put(55, crashCymbal); // Splash Cymbal
		symbolMap.put(57, crashCymbal); // Crash Cymbal 2
		
		Symbol floorTom = new Symbol("Floor Tom", 4, Color.GREEN);
		symbolList.add(floorTom);
		symbolMap.put(43, floorTom); // High Floor Tom
		symbolMap.put(45, floorTom); // Low Tom
		symbolMap.put(47, floorTom); // Low-Mid Tom

		Symbol bassDrum = new Symbol("Bass", 5, Color.PINK);
		symbolList.add(bassDrum);
		symbolMap.put(35, bassDrum); // Acoustic Bass Drum
		symbolMap.put(36, bassDrum); // Bass Drum 1
	}

	public void setInputDevice(MidiDevice.Info inputDeviceInfo) throws MidiUnavailableException {
		input.setInputDevice(inputDeviceInfo);
	}

	public List<PlaySymbol> loadFile(File midiFile) throws InvalidMidiDataException, IOException, MidiUnavailableException {
		startIndex = 0;
		
		rigthHit = 0;
		wrongHit = 0;
		timing = new Vector<long[]>();

		if (sequencer != null) {
			close();
		}

		sequence = MidiSystem.getSequence(midiFile);

		generateSymbolList();

		sequencer = MidiSystem.getSequencer();
		sequencer.addMetaEventListener(this);
		sequencer.setSequence(sequence);
		sequencer.open();

		return playSymbolList;
	}

	/*
	 * @param playSymbolList
	 */
	private void generateSymbolList() {
		playSymbolList = new Vector<PlaySymbol>();
		for (Track track : sequence.getTracks()) {
			boolean isDrums = false;

			for (int i = 0; i < track.size(); i++) {
				MidiEvent event = track.get(i);

				if (event.getMessage() instanceof ShortMessage) {
					ShortMessage message = (ShortMessage) event.getMessage();

					if (message.getCommand() == 0xc0) {
						isDrums = (message.getData2() >= 113 && message.getData2() <= 120);
					} else {
						if (message.getChannel() == 9) {
							isDrums = true;
						}
					}

					if (isDrums && message.getCommand() == 0x90 && message.getData2() > 0) {
						int key = message.getData1();
						if (symbolMap.containsKey(key)) {
							Symbol symbol = symbolMap.get(key);

							long time = MidiUtils.tick2microsecond(sequence, event.getTick(), new MidiUtils.TempoCache(sequence));
							playSymbolList.add(new PlaySymbol(symbol, time));

							track.remove(event);
							i--;

							//System.out.println(key + " " + symbol);
						} else {
							System.err.println("unknow symbol " + key + " at " + event.getTick());
						}
					}
				}
			}
		}

		Comparator<PlaySymbol> comparator = new Comparator<PlaySymbol>() {
			public int compare(PlaySymbol playSymbol1, PlaySymbol playSymbol2) {
				long diference = playSymbol1.getTime() - playSymbol2.getTime();

				if (diference > Integer.MAX_VALUE) {
					return Integer.MAX_VALUE;
				}

				if (diference < Integer.MIN_VALUE) {
					return Integer.MIN_VALUE;
				}

				return (int) diference;
			}
		};

		Collections.sort(playSymbolList, comparator);
	}

	public int getTracksLenght() {
		return sequence.getTracks().length;
	}

	public synchronized void notePlayed(int key, int velocity) {
		//synthesizer.getChannels()[9].noteOn(key, velocity);
	
		long microsecondPosition = sequencer.getMicrosecondPosition();
		
		if (velocity == 0 || !symbolMap.containsKey(key)) {
			return;
		}
	
		Symbol symbol = symbolMap.get(key);
		long mostNearTime = Long.MAX_VALUE;
		int mostNearIndex = -1;
	
		for (int i = startIndex; i < playSymbolList.size(); i++) {
			PlaySymbol playSymbol = playSymbolList.get(i);
	
			long diference = playSymbol.getTime() - microsecondPosition;
	
			if (diference < -tolerance) {
				startIndex = i + 1;
	
				continue;
			}
	
			if (diference > tolerance) {
				break;
			}
	
			if (symbol == playSymbol.getSymbol()) {
				mostNearTime = Math.min(Math.abs(mostNearTime), Math.abs(diference));
				mostNearIndex = i;
			}
		}
	
		if (mostNearIndex >= 0) {
			playSymbolList.remove(mostNearIndex);
	
			rigthHit++;
			timing.add(new long[] {microsecondPosition, microsecondPosition - mostNearTime * 1000});
		} else {
			wrongHit++;
		}
	}

	private void close() {
		sequencer.close();
	}

	public void play() {
		sequencer.start();
		timer.start();
	}

	protected void tick() {
		fireTick();
	}

	private void fireTick() {
		DrumTrainerEvent event = new DrumTrainerEvent(this, sequencer.getMicrosecondPosition());

		for (DrumTrainerListener listener : listenerList) {
			listener.tick(event);
		}
	}

	public void setTrackMute(int trackIndex, boolean mute) {
		sequencer.setTrackMute(trackIndex, mute);
	}

	public boolean isPlaying() {
		return sequencer == null ? false : sequencer.isRunning();
	}

	public long getMicrosecondLength() {
		return sequence.getMicrosecondLength();
	}

	public long getMicrosecondPosition() {
		return sequencer.getMicrosecondPosition();
	}

	public void setMicrosecondPosition(long time) {
		sequencer.setMicrosecondPosition(time);
	}

	public void setTempoFactor(long percent) {
		sequencer.setTempoFactor(percent / 100.0f);
	}

	public void meta(MetaMessage event) {
		if (event.getType() == 47) {
			stop();

			fireSongEnd();
		}
	}

	private void fireSongEnd() {
		for (DrumTrainerListener listener : listenerList) {
			listener.songEnd(new Report(rigthHit, wrongHit, playSymbolList.size(), timing));
		}
	}

	public void stop() {
		timer.stop();

		if (sequencer != null) {
			sequencer.stop();
		}
	}
}